/**
 * User: alesj
 * Date: 2004.8.11
 * Time: 10:32:12
 */

package com.generalynx.ecos.data.dao;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import com.alesj.newsfeed.bool.Condition;
import com.alesj.newsfeed.bool.ConditionCreator;
import com.alesj.newsfeed.bool.Executor;
import com.alesj.newsfeed.bool.impl.ExecutorImpl;
import com.alesj.newsfeed.data.Contact;
import com.alesj.newsfeed.data.Group;
import com.alesj.newsfeed.data.GroupEntry;
import com.alesj.newsfeed.data.Message;
import com.alesj.newsfeed.data.MessageCriteria;
import com.generalynx.common.data.IEntity;
import com.generalynx.common.userdata.IUser;
import com.generalynx.common.utils.CollectionUtils;
import com.generalynx.common.web.module.Module;
import com.generalynx.ecos.data.Role;
import com.generalynx.ecos.data.User;
import com.generalynx.ecos.data.UserRole;
import com.generalynx.ecos.data.types.IntPersistentEnumType;
import com.generalynx.ecos.data.types.Status;
import com.generalynx.ecos.page.Page;
import com.generalynx.ecos.page.PageScroll;
import com.generalynx.ecos.page.ScrollPage;
import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.Interceptor;
import org.hibernate.LockMode;
import org.hibernate.Query;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.CacheMode;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Expression;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.hibernate.type.Type;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate3.HibernateCallback;

@SuppressWarnings("unchecked")
public class HibernateBasicDAO extends HibernateFilterSupport implements IBasicDAO, IHQLStrings {

    //--------------------------------- properties -----------------------------------

    private int m_rootModuleId;
    private Interceptor interceptor;

    public int getRootModuleId() {
        return m_rootModuleId;
    }

    public void setRootModuleId(int rootModuleId) {
        m_rootModuleId = rootModuleId;
    }

    public Interceptor getInterceptor() {
        return interceptor;
    }

    public void setInterceptor(Interceptor interceptor) {
        this.interceptor = interceptor;
    }

    protected void initDao() throws Exception {
        if (getRootModuleId() == 0) {
            throw new IllegalArgumentException("Root module id required!");
        }
        if (getInterceptor() != null) {
            getHibernateTemplate().setEntityInterceptor(getInterceptor());
        }
    }

    private void buildModuleTree(Module mt, Map nameModuleMap, Map idModuleMap) {
        nameModuleMap.put(mt.getName(), mt);
        idModuleMap.put(mt.getId(), mt);

        Iterator children = mt.getChildren().iterator();
        while (children.hasNext()) {
            Module child = (Module)children.next();
            buildModuleTree(child, nameModuleMap, idModuleMap);
        }
    }
    
    //--------------------------------- DAO methods -----------------------------------


    public void evict(IEntity entity) throws DataAccessException {
        getHibernateTemplate().evict(entity);
    }

    public void initialize(Object proxy) throws DataAccessException {
        getHibernateTemplate().initialize(proxy);
    }

    public void flush() throws DataAccessException {
        getHibernateTemplate().flush();
    }

    public Module loadRootModule(Map nameModuleMap, Map idModuleMap) throws DataAccessException {
        Module root = loadModule(getRootModuleId());
        buildModuleTree(root, nameModuleMap, idModuleMap);
        return root;
    }

    public Module loadModule(int id) throws DataAccessException {
        return (Module)getHibernateTemplate().load(Module.class, new Integer(id));
    }

    public IEntity getEntity(Class clazz, int id) throws DataAccessException {
        return (IEntity)getHibernateTemplate().get(clazz, new Integer(id));
    }

    public IEntity loadEntity(Class clazz, int id) throws DataAccessException {
        return (IEntity)getHibernateTemplate().load(clazz, new Integer(id));
    }

    public IEntity loadEntity(Class clazz, int id, LockMode lockMode) throws DataAccessException {
        return (IEntity)getHibernateTemplate().load(clazz, new Integer(id), lockMode);
    }

    public void deleteEntity(IEntity entity) throws DataAccessException {
        getHibernateTemplate().delete(entity);
    }

    public void deleteEntity(IEntity entity, LockMode lockMode) throws DataAccessException {
        getHibernateTemplate().delete(entity, lockMode);
    }

    public void saveOrUpdateEntity(IEntity entity) throws DataAccessException {
        getHibernateTemplate().saveOrUpdate(entity);
    }

    public void saveOrUpdateEntity(IEntity[] entities) throws DataAccessException {
        for (int i = 0; i < entities.length; i++) {
            getHibernateTemplate().saveOrUpdate(entities[i]);
        }
    }

    public List findEntity(String queryString) throws DataAccessException {
        return findEntity(queryString, new Object[]{});
    }

    public List findEntity(String queryString, Object value) throws DataAccessException {
        return findEntity(queryString, new Object[]{value});
    }

    public List findEntity(String queryString, Object value, Type type) throws DataAccessException {
        return findEntity(queryString, new Object[]{value}, new Type[]{type});
    }

    public List findEntity(String queryString, Object[] values) throws DataAccessException {
        return getHibernateTemplate().find(queryString, values);
    }

    public List findEntity(final String queryString, final Object[] values, final Type[] types) throws DataAccessException {
        return getHibernateTemplate().executeFind(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException {
                Query query = session.createQuery(queryString);
                for (int i = 0; i < values.length; i++) {
                    query.setParameter(i, values[i], types[i]);
                }
                return query.list();
            }
        });
    }

    public List findEntity(final PageScroll pageScroll) throws DataAccessException {
        return getHibernateTemplate().executeFind(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException {
                Query query = session.createQuery(pageScroll.query());
                //first do total count
                if (pageScroll.hasTotalCount()) {
                    ScrollableResults sr = query.scroll();
                    try {
                        sr.last();
                        pageScroll.setTotalCount(sr.getRowNumber());
                    } finally {
                        sr.close();
                    }
                }
                //set limits
                if (pageScroll.isLimitable()) {
                    query.setFirstResult(pageScroll.firstResult());
                    query.setMaxResults(pageScroll.getLimit());
                } else { // set params
                    query.setProperties(pageScroll.propertiesBean());
                }
                query.setResultTransformer(pageScroll.transformer());
                return query.list();
            }
        });
    }

    public List findEntity(final Class entity) throws DataAccessException {
        return getHibernateTemplate().executeFind(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException {
                return session.createCriteria(entity).list();
            }
        });
    }

    public List findEntityWithUser(final Class entity, final IUser user) throws DataAccessException {
        /*
        return getHibernateTemplate().executeFind(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException {
                Criteria criteria = session.createCriteria(entity);
                //to-do missing in elements(...)
                return criteria.list();
            }
        });
        */
        return getHibernateTemplate().find("from " + entity.getName() + " e where ? in elements(e.users)", user);
    }

    public List findEntityByCreator(final Class entity, final IUser creator) throws DataAccessException {
        return getHibernateTemplate().executeFind(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException {
                Criteria criteria = session.createCriteria(entity);
                criteria.add(Expression.eq("creator", creator));
                return criteria.list();
            }
        });
    }

    public User loadUser(int id) throws DataAccessException {
        return (User)loadEntity(User.class, id);
    }

    public List findUser(String username) throws DataAccessException {
        return getHibernateTemplate().find("from User u where u.username = ?", username);
    }

    public List findUser(String username, String role) throws DataAccessException {
        return getHibernateTemplate().find(
                "select u" +
                " from User u, Role r" +
                " where u.username = ?" +
                " and r.rolename = ?" +
                " and r in elements(u.roles)",
                new String[]{username, role});
    }

    public List findUser(String username, String password, String role) throws DataAccessException {
        return getHibernateTemplate().find(
                "select u" +
                " from User u, Role r" +
                " where u.username = ?" +
                " and u.password = ?" +
                " and r.rolename = ?" +
                " and r in elements(u.roles)",
                new String[]{username, password, role});
    }

    public List findUsers() throws DataAccessException {
        return getHibernateTemplate().find("from User u");
    }

    public List findUsers(String role) throws DataAccessException {
        return getHibernateTemplate().find("from User u, Role r where r.rolename = ? and r in elements(u.roles)");
    }

    public Role loadRole(int id) throws DataAccessException {
        return (Role)getHibernateTemplate().load(Role.class, new Integer(id));
    }

    public Role loadRole(String roleName) throws DataAccessException {
        List list = getHibernateTemplate().find("from Role r where r.rolename = ?", roleName);
        return (Role)(list.isEmpty() ? null : list.get(0));
    }

    public List findRoles() throws DataAccessException {
        return getHibernateTemplate().find("from Role r");
    }

    public UserRole loadUserRole(int userId, int roleId) throws DataAccessException {
        UserRole userRole = new UserRole();
        userRole.setUserId(userId);
        userRole.setRoleId(roleId);
        getHibernateTemplate().load(userRole, userRole);
        return userRole;
    }

    public void saveUserRole(UserRole userRole) throws DataAccessException {
        getHibernateTemplate().saveOrUpdate(userRole);
    }

    public void deleteUserRole(UserRole userRole) throws DataAccessException {
        getHibernateTemplate().delete(userRole);
    }

    public Page loadPage(final String hql, final int pageNumber, final int pageSize) throws DataAccessException {
        return (Page)getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException {
                Query query = session.createQuery(hql);
                return new ScrollPage(query, pageNumber, pageSize);
            }
        });
    }

    public Page loadPage(final DetachedCriteria detachedCriteria, final int pageNumber, final int pageSize)
            throws DataAccessException {
        return (Page)getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException {
                Criteria criteria = detachedCriteria.getExecutableCriteria(session);  
                return new ScrollPage(criteria, pageNumber, pageSize);
            }
        });
    }

    /* Contacts */

    public Contact getContact(int contactId) throws DataAccessException {
        return (Contact)getEntity(Contact.class, contactId);
    }

    public Contact loadContact(int contactId) throws DataAccessException {
        return (Contact)loadEntity(Contact.class, contactId);
    }

    protected static Type STATUS_TYPE = Hibernate.custom(IntPersistentEnumType.class,
                                                      new String[]{"enumClass"},
                                                      new String[]{Status.class.getName()});

    public List findContacts(int[] entries, int[] excludes, final boolean isConjunction) throws DataAccessException {
        return findContacts(entries, excludes, isConjunction, new Status[]{Status.DEFAULT});
    }

    public List findContacts(int[] entries, int[] excludes, final boolean isConjunction, final Status[] acceptedStatus)
            throws DataAccessException {
        Set excluded = CollectionUtils.asSet(excludes, new TreeSet());

        final Set<GroupEntry> includedEntries = new HashSet<GroupEntry>();
        final Set<GroupEntry> excludedEntries = new HashSet<GroupEntry>();

        for (int i = 0; entries != null && i < entries.length; i++) {
            if (entries[i] > 0) {
                GroupEntry entry = loadGroupEntry(entries[i]);
                if (excluded.contains(entry.getGroup().getId())) {
                    excludedEntries.add(entry);
                } else {
                    includedEntries.add(entry);
                }
            } else {
                Group group = loadGroup(-entries[i]);
                if (excluded.contains(group.getId())) {
                    excludedEntries.addAll(group.getEntries());
                } else {
                    includedEntries.addAll(group.getEntries());
                }
            }
        }

        return getHibernateTemplate().executeFind(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException {
                // contact can have just one status - list is ok to distinct between them
                List<Contact> allContacts = null;
                for (Status status : acceptedStatus) {
                    Set<Contact> contacts = null;
                    for(GroupEntry entry : includedEntries) {
                        contacts = doFindMemberContacts(session,
                                                  entry,
                                                  contacts,
                                                  isConjunction,
                                                  status);
                    }
                    for(GroupEntry entry : excludedEntries) {
                        contacts = doFindExcludedContacts(session,
                                                  entry,
                                                  contacts,
                                                  isConjunction,
                                                  status);
                    }
                    if (contacts != null && !contacts.isEmpty()) {
                        if (allContacts == null)
                            allContacts = new ArrayList<Contact>();
                        allContacts.addAll(contacts);
                    }
                }
                return (allContacts != null ? allContacts : null);
            }
        });
    }

    protected Set<Contact> doFindMemberContacts(Session session,
                               GroupEntry entry,
                               Set contacts,
                               boolean isConjunction,
                               Status status) {
        Query query = session
                .createFilter(entry.getContacts(), "select this where this.status = :status")
                .setParameter("status", status, STATUS_TYPE);
        return joinContacts(contacts, query, isConjunction);
    }

    // todo
    protected Set<Contact> doFindExcludedContacts(Session session,
                               GroupEntry entry,
                               Set contacts,
                               boolean isConjunction,
                               Status status) {
        Query query = session
                .createQuery("from Contact c where :ge not in elements (c.entries) and c.status = :status")
                .setEntity("ge", entry)
                .setParameter("status", status, STATUS_TYPE);
        return joinContacts(contacts, query, isConjunction);
    }

    protected Set<Contact> joinContacts(Set contacts, Query query, boolean isConjunction) {
        if (contacts == null) {
            contacts = new HashSet<Contact>();
            contacts.addAll(query.list());
        } else if (isConjunction) {
            contacts.retainAll(query.list());
        } else {
            contacts.addAll(query.list());
        }
        return contacts;
    }

    /* Group */

    public Group loadGroup(int id) throws DataAccessException {
        return (Group)loadEntity(Group.class, id);
    }

    /* Group Entries */

    public GroupEntry loadGroupEntry(int id) throws DataAccessException {
        return (GroupEntry)loadEntity(GroupEntry.class, id);
    }

    /* Messages */

    public Message loadMessage(int messageId) throws DataAccessException {
        return (Message)loadEntity(Message.class, messageId);
    }

    public List findMessages(final String subject,
                             final Date from,
                             final Date till,
                             final User creator,
                             final int failed)
            throws DataAccessException {
        return getHibernateTemplate().executeFind(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException {
                Criteria criteria = session.createCriteria(Message.class);
                if (subject != null) {
                    criteria.add(Restrictions.ilike("subject", "%" + subject + "%"));
                }
                if (from != null) {
                    criteria.add(Restrictions.ge("creationTime", from));
                }
                if (till != null) {
                    criteria.add(Restrictions.le("creationTime", till));
                }
                if (creator != null) {
                    criteria.add(Restrictions.eq("creator", creator));
                }
                if (failed < 0) {
                    // some successful hits
                    criteria.add(Restrictions.isNotEmpty("recepients"));
                } else if (failed > 0) {
                    // some unsuccessful hits
                    criteria.add(Restrictions.isNotEmpty("failed"));
                }
                criteria.addOrder(Order.desc("creationTime"));
                return criteria.list();
            }
        });
    }

    public List findContacts(String textQuery, Set<MessageCriteria>criteria) throws DataAccessException {
        return findContacts(textQuery, criteria, new Status[]{Status.DEFAULT});
    }

    public List findContacts(final String textQuery, final Set<MessageCriteria>criteria, final Status[] acceptedStatus) throws DataAccessException {
        return getHibernateTemplate().executeFind(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                List<Contact> contacts = new ArrayList<Contact>();
                for(Status status : acceptedStatus) {
                    Executor<Contact> executor = new ExecutorImpl<Contact>(new ContactLookupConditionCreator(session, status));
                    contacts.addAll(executor.execute(textQuery));
                }
                return contacts.isEmpty() ? null : contacts;
            }
        });
    }

    private class ContactLookupConditionCreator implements ConditionCreator<Contact> {
        private Session session;
        private Status status;

        public ContactLookupConditionCreator(Session session, Status status) {
            this.session = session;
            this.status = status;
        }

        public Set<Contact> all() {
            List<Contact> contacts = session
                    .createCriteria(Contact.class)
                    .setCacheable(true)
                    .setCacheMode(CacheMode.NORMAL)
                    .list();
            return new HashSet<Contact>(contacts);
        }

        public Condition<Contact> createCondition(String condition) {
            GroupEntry entry = (GroupEntry)session
                    .createCriteria(GroupEntry.class)
                    .add(Restrictions.eq("name", condition))
                    .uniqueResult();
            if (entry == null)
                throw new IllegalArgumentException("No such group entry with name: " + condition);
            return new ContactCondition(entry);
        }

        class ContactCondition implements Condition<Contact> {
            private GroupEntry entry;

            public ContactCondition(GroupEntry entry) {
                this.entry = entry;
            }

            public Set<Contact> execute() {
                Query filter = session
                        .createFilter(entry.getContacts(), "select this where this.status = :status")
                        .setParameter("status", status, STATUS_TYPE);
                return new HashSet<Contact>(filter.list());
            }
        }
    }

}
